import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import type { IProject, ProjectRenderMeta } from '@hey-api/codegen-core';

import type { DefinePlugin } from '~/plugins';
import type { Client } from '~/plugins/@hey-api/client-core/types';
import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils';
import type { Config } from '~/types/config';

import { ensureDirSync } from './utils';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
 * Returns absolute path to the client folder. This is hard-coded for now.
 */
export const clientFolderAbsolutePath = (config: Config): string => {
  const client = getClientPlugin(config);

  if ('bundle' in client.config && client.config.bundle) {
    // not proud of this one
    const renamed: Map<string, string> | undefined =
      // @ts-expect-error
      config._FRAGILE_CLIENT_BUNDLE_RENAMED;
    return path.resolve(
      config.output.path,
      'client',
      `${renamed?.get('index') ?? 'index'}.ts`,
    );
  }

  return client.name;
};

/**
 * Recursively copies files and directories.
 * This is a PnP-compatible alternative to fs.cpSync that works with Yarn PnP's
 * virtualized filesystem.
 */
const copyRecursivePnP = (src: string, dest: string) => {
  const stat = fs.statSync(src);

  if (stat.isDirectory()) {
    if (!fs.existsSync(dest)) {
      fs.mkdirSync(dest, { recursive: true });
    }

    const files = fs.readdirSync(src);
    for (const file of files) {
      copyRecursivePnP(path.join(src, file), path.join(dest, file));
    }
  } else {
    const content = fs.readFileSync(src);
    fs.writeFileSync(dest, content);
  }
};

const renameFile = ({
  filePath,
  project,
  renamed,
}: {
  filePath: string;
  project: IProject;
  renamed: Map<string, string>;
}) => {
  const extension = path.extname(filePath);
  const name = path.basename(filePath, extension);
  const renamedName = project.fileName?.(name) || name;
  if (renamedName !== name) {
    const outputPath = path.dirname(filePath);
    fs.renameSync(
      filePath,
      path.resolve(outputPath, `${renamedName}${extension}`),
    );
    renamed.set(name, renamedName);
  }
};

const replaceImports = ({
  filePath,
  meta,
  renamed,
}: {
  filePath: string;
  meta: ProjectRenderMeta;
  renamed: Map<string, string>;
}) => {
  let content = fs.readFileSync(filePath, 'utf8');

  content = content.replace(
    /from\s+['"](\.\.?\/[^'"]*?)['"]/g,
    (match, importPath) => {
      const importIndex = match.indexOf(importPath);
      const extension = path.extname(importPath);
      const fileName = path.basename(importPath, extension);
      const importDir = path.dirname(importPath);
      const replacedName =
        (renamed.get(fileName) ?? fileName) +
        (meta.importFileExtension ? meta.importFileExtension : extension);
      const replacedMatch =
        match.slice(0, importIndex) +
        [importDir, replacedName].filter(Boolean).join('/') +
        match.slice(importIndex + importPath.length);
      return replacedMatch;
    },
  );

  const header = '// This file is auto-generated by @hey-api/openapi-ts\n\n';

  content = `${header}${content}`;

  fs.writeFileSync(filePath, content, 'utf8');
};

/**
 * Creates a `client` folder containing the same modules as the client package.
 */
export const generateClientBundle = ({
  meta,
  outputPath,
  plugin,
  project,
}: {
  meta: ProjectRenderMeta;
  outputPath: string;
  plugin: DefinePlugin<Client.Config & { name: string }>['Config'];
  project?: IProject;
}): Map<string, string> | undefined => {
  const renamed = new Map<string, string>();

  // copy Hey API clients to output
  const isHeyApiClientPlugin = plugin.name.startsWith('@hey-api/client-');
  if (isHeyApiClientPlugin) {
    // copy client core
    const coreOutputPath = path.resolve(outputPath, 'core');
    ensureDirSync(coreOutputPath);
    const coreDistPath = path.resolve(__dirname, 'clients', 'core');
    copyRecursivePnP(coreDistPath, coreOutputPath);

    // copy client bundle
    const clientOutputPath = path.resolve(outputPath, 'client');
    ensureDirSync(clientOutputPath);
    const clientDistFolderName = plugin.name.slice('@hey-api/client-'.length);
    const clientDistPath = path.resolve(
      __dirname,
      'clients',
      clientDistFolderName,
    );
    copyRecursivePnP(clientDistPath, clientOutputPath);

    if (project) {
      const copiedCoreFiles = fs.readdirSync(coreOutputPath);
      for (const file of copiedCoreFiles) {
        renameFile({
          filePath: path.resolve(coreOutputPath, file),
          project,
          renamed,
        });
      }

      const copiedClientFiles = fs.readdirSync(clientOutputPath);
      for (const file of copiedClientFiles) {
        renameFile({
          filePath: path.resolve(clientOutputPath, file),
          project,
          renamed,
        });
      }
    }

    const coreFiles = fs.readdirSync(coreOutputPath);
    for (const file of coreFiles) {
      replaceImports({
        filePath: path.resolve(coreOutputPath, file),
        meta,
        renamed,
      });
    }

    const clientFiles = fs.readdirSync(clientOutputPath);
    for (const file of clientFiles) {
      replaceImports({
        filePath: path.resolve(clientOutputPath, file),
        meta,
        renamed,
      });
    }
    return renamed;
  }

  const clientSrcPath = path.isAbsolute(plugin.name)
    ? path.dirname(plugin.name)
    : undefined;

  // copy custom local client to output
  if (clientSrcPath) {
    const dirPath = path.resolve(outputPath, 'client');
    ensureDirSync(dirPath);
    copyRecursivePnP(clientSrcPath, dirPath);
    return;
  }

  // copy third-party client to output
  const clientModulePath = path.normalize(require.resolve(plugin.name));
  const clientModulePathComponents = clientModulePath.split(path.sep);
  const clientDistPath = clientModulePathComponents
    .slice(0, clientModulePathComponents.indexOf('dist') + 1)
    .join(path.sep);

  const indexJsFile =
    clientModulePathComponents[clientModulePathComponents.length - 1];
  const distFiles = [indexJsFile!, 'index.d.mts', 'index.d.cts'];
  const dirPath = path.resolve(outputPath, 'client');
  ensureDirSync(dirPath);
  for (const file of distFiles) {
    fs.copyFileSync(
      path.resolve(clientDistPath, file),
      path.resolve(dirPath, file),
    );
  }

  return;
};
